using System;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;

namespace ClieOp.Record
{
    internal class Record
    {
        protected const string NewLine = "\r\n";
        private FieldLayout _fields;

        /// <summary>
        /// Initializes a new instance of the Record class.
        /// </summary>
        /// <param name="recordType">Type of the record.</param>
        public Record(RecordType recordType)
        {
            RecordType = recordType;
        }

        private FieldLayout Fields
        {
            get { return _fields ?? (_fields = new FieldLayout(RecordLayout.Instance[RecordType])); }
        }

        /// <summary>
        /// Gets the record code.
        /// </summary>
        public long Recordcode
        {
            get { return Convert.ToInt64(this[FieldName.Recordcode].Value); }
        }

        /// <summary>
        /// Gets the type of the record.
        /// </summary>
        public RecordType RecordType { get; private set; }

        /// <summary>
        /// Gets the ClieOp.Record.Field with the specified name.
        /// </summary>
        public Field this[FieldName name]
        {
            get { return Fields[name]; }
        }

        /// <summary>
        /// Returns a System.String that represents this instance.
        /// </summary>
        public new string ToString()
        {
            var sb = new StringBuilder();
            foreach (Field field in Fields.Values)
            {
                sb.Append(field.ToString());
            }
            sb.Append(NewLine);
            return sb.ToString();
        }

        /// <summary>
        /// Parses the specified record line.
        /// </summary>
        /// <typeparam name="T">A subclass of Record.</typeparam>
        /// <param name="recordLine">The record line as a string.</param>
        /// <returns>A parsed record of type T.</returns>
        public static T Parse<T>(string recordLine) where T : Record, new()
        {
            var newRecord = new T();
            recordLine = recordLine.TrimEnd('\r', '\n');
            foreach (Field field in newRecord.Fields.Values)
            {
                recordLine = TakeFieldFromRecordLine(recordLine, field);
            }
            return newRecord;
        }

        /// <summary>
        /// Parses the specified record line.
        /// </summary>
        /// <param name="recordLine">The record line as a string.</param>
        /// <returns>A parsed record. The object's underlying type matches the record's Recordcode.</returns>
        /// <exception cref="ArgumentException">Error while parsing record line. The line does not start with a recordcode (4 digits).</exception>
        /// <exception cref="Exception">Error while parsing record line. Record code NNNN does not refer to an existing RecordType.</exception>
        public static Record Parse(string recordLine)
        {
            if (!Regex.IsMatch(recordLine, @"^\d{4}"))
            {
                throw new ArgumentException(
                    string.Format("Error while parsing record line. The line does not start with a recordcode (4 digits). Line value: `{0}'.", recordLine),
                    "recordLine");
            }
            long recordCode = long.Parse(recordLine.Substring(0, 4));
            if (!RecordLayout.Instance.ContainsKey(recordCode))
            {
                throw new Exception(string.Format("Error while parsing record line. Record code {0} does not refer to an existing RecordType.", recordCode));
            }
            string typeName = string.Format("{0}.{1}", typeof (Record).Namespace, Enum.GetName(typeof (RecordType), RecordLayout.Instance[recordCode]));
            MethodInfo methodInfo = GetGenericMethod(typeof (Record), "Parse");
            return (Record)methodInfo.MakeGenericMethod(Type.GetType(typeName)).Invoke(null, new object[] {recordLine});
        }

        /// <exception cref="Exception">Expected field length NN, but there are only MM characters remaining in the record line.</exception>
        /// <exception cref="Exception">Error while parsing field.</exception>
        private static string TakeFieldFromRecordLine(string recordLine, Field field)
        {
            object fieldValue;
            try
            {
                if (field.Length > recordLine.Length)
                {
                    throw new Exception(string.Format("Expected field length {0}, but there are only {1} characters remaining in the record line.", field.Length,
                                                      recordLine.Length));
                }
                string fieldValueString = recordLine.Substring(0, field.Length);
                fieldValue = field.Type == FieldType.Type9 ? (object)long.Parse(fieldValueString) : fieldValueString;
            }
            catch (Exception e)
            {
                throw new Exception(
                    string.Format("Error while parsing field `{0}', type {1}. {2}", Enum.GetName(typeof (FieldName), field.Name),
                                  Enum.GetName(typeof (FieldType), field.Type), e.Message), e);
            }
            if (!field.IsReadOnly)
            {
                field.Value = fieldValue;
            }
            else if (field != fieldValue)
            {
                throw new Exception(
                    string.Format("Error while parsing field `{0}'. This field is read-only and should have value `{1}', but the parsed value is `{2}'.",
                                  Enum.GetName(typeof (FieldName), field.Name), field.Value, fieldValue));
            }
            return recordLine.Substring(field.Length);
        }

        /// <exception cref="Exception">Could not find method with signature METHODNAME (of Type type).</exception>
        private static MethodInfo GetGenericMethod(Type type, string methodName)
        {
            foreach (MethodInfo methodInfo in type.GetMethods())
            {
                if (methodInfo.Name == methodName && methodInfo.ContainsGenericParameters)
                {
                    return methodInfo;
                }
            }
            throw new Exception(string.Format("Could not find method with signature `{0}<T>( ... )'.", methodName));
        }
    }
}